/**
 * Anderground project - Subway maps processor application.
 * Copyright (C) 2008-2009 Eldar Abusalimov, Ilya Shurigyn
 * 
 * This file is part of Anderground.
 * 
 * Anderground is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License
 * as published by the Free Software Foundation; either version 2
 * of the License, or (at your option) any later version.
 * 
 * Anderground is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 * 
 * You should have received a copy of the GNU General Public License
 * along with Anderground.  If not, see <http://www.gnu.org/licenses/>.
 */

package org.anderground.modules.storage.xml;

import static org.anderground.modules.storage.xml.XmlModelStorage.ATTR_ANGLE;
import static org.anderground.modules.storage.xml.XmlModelStorage.ATTR_CLOSE;
import static org.anderground.modules.storage.xml.XmlModelStorage.ATTR_COLOR;
import static org.anderground.modules.storage.xml.XmlModelStorage.ATTR_COUNTRY_CODE;
import static org.anderground.modules.storage.xml.XmlModelStorage.ATTR_DX;
import static org.anderground.modules.storage.xml.XmlModelStorage.ATTR_DY;
import static org.anderground.modules.storage.xml.XmlModelStorage.ATTR_FROM;
import static org.anderground.modules.storage.xml.XmlModelStorage.ATTR_ID;
import static org.anderground.modules.storage.xml.XmlModelStorage.ATTR_LATITUDE;
import static org.anderground.modules.storage.xml.XmlModelStorage.ATTR_LINE;
import static org.anderground.modules.storage.xml.XmlModelStorage.ATTR_LONGITUDE;
import static org.anderground.modules.storage.xml.XmlModelStorage.ATTR_OPEN;
import static org.anderground.modules.storage.xml.XmlModelStorage.ATTR_SMOOTH;
import static org.anderground.modules.storage.xml.XmlModelStorage.ATTR_TIME;
import static org.anderground.modules.storage.xml.XmlModelStorage.ATTR_TO;
import static org.anderground.modules.storage.xml.XmlModelStorage.ATTR_VERSION;
import static org.anderground.modules.storage.xml.XmlModelStorage.ATTR_VISIBLE;
import static org.anderground.modules.storage.xml.XmlModelStorage.ATTR_X;
import static org.anderground.modules.storage.xml.XmlModelStorage.ATTR_Y;
import static org.anderground.modules.storage.xml.XmlModelStorage.TAG_ALIAS;
import static org.anderground.modules.storage.xml.XmlModelStorage.TAG_CITY;
import static org.anderground.modules.storage.xml.XmlModelStorage.TAG_CONTENT;
import static org.anderground.modules.storage.xml.XmlModelStorage.TAG_COUNTRY;
import static org.anderground.modules.storage.xml.XmlModelStorage.TAG_DESCRIPTION;
import static org.anderground.modules.storage.xml.XmlModelStorage.TAG_EMAIL;
import static org.anderground.modules.storage.xml.XmlModelStorage.TAG_LINE;
import static org.anderground.modules.storage.xml.XmlModelStorage.TAG_LINES;
import static org.anderground.modules.storage.xml.XmlModelStorage.TAG_LOCATION;
import static org.anderground.modules.storage.xml.XmlModelStorage.TAG_NAME;
import static org.anderground.modules.storage.xml.XmlModelStorage.TAG_NOTE;
import static org.anderground.modules.storage.xml.XmlModelStorage.TAG_POSITION;
import static org.anderground.modules.storage.xml.XmlModelStorage.TAG_ROOT;
import static org.anderground.modules.storage.xml.XmlModelStorage.TAG_SCHEDULE;
import static org.anderground.modules.storage.xml.XmlModelStorage.TAG_STATION;
import static org.anderground.modules.storage.xml.XmlModelStorage.TAG_STATIONS;
import static org.anderground.modules.storage.xml.XmlModelStorage.TAG_TRANSFER;
import static org.anderground.modules.storage.xml.XmlModelStorage.TAG_TRANSFERS;
import static org.anderground.modules.storage.xml.XmlModelStorage.TAG_TUNNEL;
import static org.anderground.modules.storage.xml.XmlModelStorage.TAG_TUNNELS;
import static org.anderground.modules.storage.xml.XmlModelStorage.VERSION_0_6;

import java.io.File;
import java.io.IOException;
import java.io.InputStream;

import javax.xml.parsers.ParserConfigurationException;
import javax.xml.parsers.SAXParser;
import javax.xml.parsers.SAXParserFactory;

import org.anderground.core.Logger;
import org.anderground.core.model.ModelData;
import org.anderground.core.model.ModelException;
import org.anderground.core.storage.StorageException;
import org.anderground.core.util.ColorUtils;
import org.anderground.core.util.Time;
import org.anderground.core.util.TimeFormatException;
import org.xml.sax.Attributes;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;
import org.xml.sax.XMLReader;

/**
 * {@link XmlModelReader} implementation that uses SAX for XML parsing.
 * 
 * @author Eldar Abusalimov (eldar.abusalimov@gmail.com)
 */
public class SaxModelReader extends XmlModelReader {

	/**
	 * Constructs a new {@link SaxModelReader} that uses the specified
	 * {@link File} for the future reading.
	 * 
	 * @param file
	 *            the file to read from
	 */
	public SaxModelReader(File file) {
		super(file);
	}

	/**
	 * Constructs a new {@link SaxModelReader} that uses a {@link File} created
	 * by converting the specified path string.
	 * 
	 * @param path
	 *            the path of a file to read a data from
	 */
	public SaxModelReader(String path) {
		super(path);
	}

	/**
	 * Constructs a new {@link SaxModelReader} that uses the specified
	 * {@link InputStream} for the future reading.
	 * 
	 * @param inputStream
	 *            the stream to read from
	 * @param closeAfterRead
	 *            defines whether to close the stream after {@link #read()}
	 *            completes
	 */
	public SaxModelReader(InputStream inputStream, boolean closeAfterRead) {
		super(inputStream, closeAfterRead);
	}

	@Override
	protected ModelData readFromInput(InputStream inputStream)
			throws IOException {

		try {
			try {
				SAXParserFactory parserFactory = SAXParserFactory.newInstance();
				parserFactory.setFeature(
						"http://xml.org/sax/features/namespaces", false);
				parserFactory.setFeature(
						"http://xml.org/sax/features/namespace-prefixes", true);
				SAXParser parser = parserFactory.newSAXParser();

				XMLReader reader = parser.getXMLReader();

				reader.setFeature("http://xml.org/sax/features/namespaces",
						false);
				reader.setFeature(
						"http://xml.org/sax/features/namespace-prefixes", true);

				SaxModelBuilder builder = new SaxModelBuilder();
				reader.setContentHandler(new RootHierarchyHandler(reader,
						builder));

				InputSource inputSource = new InputSource(inputStream);
				reader.parse(inputSource);

				return builder.build();

			} catch (SAXException e) {
				if (e.getException() instanceof ModelException) {
					throw (ModelException) e.getException();
				} else {
					throw new StorageException("SAX parsing failed", e);
				}
			} catch (ParserConfigurationException e) {
				throw new StorageException(e);
			}

		} catch (ModelException e) {
			Logger.e("unexpected situation", e);
			throw new StorageException(
					"unexpected ModelException during the reading", e);
		}

	}

	/**
	 * @author Eldar Abusalimov (eldar.abusalimov@gmail.com)
	 */
	private static interface HandlerCommons {

		public abstract XMLReader getReader();

		public abstract SaxModelBuilder getBuilder();

		public abstract String getVersion();

	}

	/**
	 * @author Eldar Abusalimov (eldar.abusalimov@gmail.com)
	 */
	private static abstract class AbstractHierarchyHandler extends
			HierarchyHandler implements HandlerCommons {

		@Override
		protected final void enter(HierarchyHandler childHandler) {
			super.enter(childHandler);
			if (childHandler instanceof HandlerCommons) {
				getReader().setContentHandler(childHandler);
			}
		}

		@Override
		protected final HierarchyHandler leave() {
			HierarchyHandler handler = super.leave();
			getReader().setContentHandler(handler);
			return handler;
		}

		@Override
		protected void setRoot(HierarchyHandler root) {
			if (root instanceof AbstractHierarchyHandler) {
				setRoot((AbstractHierarchyHandler) root);
			}
		}

		protected abstract void setRoot(AbstractHierarchyHandler root);

		protected abstract AbstractHierarchyHandler getRoot();

	}

	/**
	 * @author Eldar Abusalimov (eldar.abusalimov@gmail.com)
	 */
	private static abstract class ChildHierarchyHandler extends
			AbstractHierarchyHandler {

		private AbstractHierarchyHandler root;

		@Override
		public SaxModelBuilder getBuilder() {
			return root.getBuilder();
		}

		@Override
		public XMLReader getReader() {
			return root.getReader();
		}

		@Override
		protected AbstractHierarchyHandler getRoot() {
			return root;
		}

		@Override
		public String getVersion() {
			return root.getVersion();
		}

		@Override
		protected void setRoot(AbstractHierarchyHandler root) {
			this.root = root;
		}

	}

	/**
	 * @author Eldar Abusalimov (eldar.abusalimov@gmail.com)
	 */
	private final static class RootHierarchyHandler extends
			AbstractHierarchyHandler {

		private XMLReader reader;
		private SaxModelBuilder builder;

		private String version = null;

		/**
		 * @param reader
		 * @param builder
		 */
		public RootHierarchyHandler(XMLReader reader, SaxModelBuilder builder) {
			this.reader = reader;
			this.builder = builder;
		}

		@Override
		protected HierarchyHandler onStartChild(String name,
				Attributes attributes) {
			if (name.equals(TAG_ROOT)) {
				this.version = attributes.getValue(ATTR_VERSION);
				return new MapHandler();

			}
			return this;
		}

		/**
		 * @return the version
		 */
		public final String getVersion() {
			return version;
		}

		/**
		 * @return the reader
		 */
		public final XMLReader getReader() {
			return reader;
		}

		/**
		 * @return the builder
		 */
		public final SaxModelBuilder getBuilder() {
			return builder;
		}

		@Override
		protected AbstractHierarchyHandler getRoot() {
			return this;
		}

		@Override
		protected void setRoot(AbstractHierarchyHandler root) {
			// noop
		}

	}

	/**
	 * @author Eldar Abusalimov (eldar.abusalimov@gmail.com)
	 */
	private static class MapHandler extends ChildHierarchyHandler {

		@Override
		protected HierarchyHandler onStartChild(String name,
				Attributes attributes) {
			if (name.equals(TAG_STATIONS)) {
				return new StationsHandler();

			} else if (name.equals(TAG_LINES)) {
				return new LinesHandler();

			} else if (name.equals(TAG_TRANSFERS)) {
				return new TransfersHandler();

			} else if (name.equals(TAG_TUNNELS)) {
				return new TunnelsHandler();

			} else if (name.equals(TAG_DESCRIPTION)) {
				return new DescriptionHandler();

			} else if (name.equals(TAG_CITY)) {
				return new CityHandler();

			}
			return this;
		}

	}

	/**
	 * @author Eldar Abusalimov (eldar.abusalimov@gmail.com)
	 */
	private static class StationsHandler extends ChildHierarchyHandler {

		@Override
		protected HierarchyHandler onStartChild(String name, Attributes attrs)
				throws SAXException {
			if (name.equals(TAG_STATION)) {
				try {
					int id = Integer.parseInt(attrs.getValue(ATTR_ID));
					getBuilder().startStation(id);
				} catch (NumberFormatException e) {
					// do not process a station with bad id attribute
					return new SilentHandler();
				}

			} else if (name.equals(TAG_SCHEDULE)) {
				try {
					getBuilder().setStationSchedule(
							new Time(attrs.getValue(ATTR_OPEN)),
							new Time(attrs.getValue(ATTR_CLOSE)));
				} catch (TimeFormatException e) {
					// noop
				}

			} else if (name.equals(TAG_NAME)) {
				try {
					int dx = Integer.parseInt(attrs.getValue(ATTR_DX));
					int dy = Integer.parseInt(attrs.getValue(ATTR_DY));
					int angle = Integer.parseInt(attrs.getValue(ATTR_ANGLE));
					boolean visible = Boolean.parseBoolean(attrs
							.getValue(ATTR_VISIBLE));
					getBuilder().setStationNameAttrs(dx, dy, angle, visible);
				} catch (NumberFormatException e) {
					// noop
				}

			} else if (name.equals(TAG_POSITION)) {
				try {
					int x = Integer.parseInt(attrs.getValue(ATTR_X));
					int y = Integer.parseInt(attrs.getValue(ATTR_Y));
					getBuilder().setStationPosition(x, y);
				} catch (NumberFormatException e) {
					// noop
				}

			} else if (name.equals(TAG_LOCATION)) {
				try {
					int latitude = Integer.parseInt(attrs
							.getValue(ATTR_LATITUDE));
					int longitude = Integer.parseInt(attrs
							.getValue(ATTR_LONGITUDE));
					getBuilder().setStationLocation(latitude, longitude);
				} catch (NumberFormatException e) {
					// noop
				}

			}
			return this;
		}

		@Override
		protected void onNonWhiteString(String name, String string)
				throws SAXException {
			if (name.equals(TAG_NAME)) {
				getBuilder().setStationName(string);

			}
		}

		@Override
		protected void onEndChild(HierarchyHandler childHandler, String name)
				throws SAXException {
			try {
				if (name.equals(TAG_STATION)) {
					getBuilder().endStation();

				}
			} catch (ModelException e) {
				throw new SAXException(e);
			}
		}

	}

	/**
	 * @author Eldar Abusalimov (eldar.abusalimov@gmail.com)
	 */
	private static class LinesHandler extends ChildHierarchyHandler {

		@Override
		protected HierarchyHandler onStartChild(String name, Attributes attrs)
				throws SAXException {
			if (name.equals(TAG_LINE)) {
				try {
					int id = Integer.parseInt(attrs.getValue(ATTR_ID));
					getBuilder().startLine(id);
					try {
						getBuilder().setLineColor(
								ColorUtils.parse(attrs.getValue(ATTR_COLOR)));
					} catch (NumberFormatException e) {
						getBuilder().setLineColor(ColorUtils.WHITE);
					}
				} catch (NumberFormatException e) {
					// do not process a line with bad id attribute
					return new SilentHandler();
				}

			}
			return this;
		}

		@Override
		protected void onNonWhiteString(String name, String string)
				throws SAXException {
			if (name.equals(TAG_NAME)) {
				getBuilder().setLineName(string);

			}
		}

		@Override
		protected void onEndChild(HierarchyHandler childHandler, String name)
				throws SAXException {
			if (name.equals(TAG_LINE)) {
				getBuilder().endLine();

			}
		}

	}

	/**
	 * @author Eldar Abusalimov (eldar.abusalimov@gmail.com)
	 */
	private static class TunnelsHandler extends ChildHierarchyHandler {

		@Override
		protected HierarchyHandler onStartChild(String name, Attributes attrs)
				throws SAXException {
			if (name.equals(TAG_TUNNEL)) {
				try {
					int from = Integer.parseInt(attrs.getValue(ATTR_FROM));
					int to = Integer.parseInt(attrs.getValue(ATTR_TO));
					int line = Integer.parseInt(attrs.getValue(ATTR_LINE));

					int time = 0;
					boolean smooth = Boolean.parseBoolean(attrs
							.getValue(ATTR_SMOOTH));
					try {
						time = Time.stringToSeconds(attrs.getValue(ATTR_TIME));
					} catch (TimeFormatException e) {
						// noop
					}

					getBuilder().addTunnel(from, to, time, line, smooth);
				} catch (NumberFormatException e) {
					// noop
				}

			}
			return this;
		}

	}

	/**
	 * @author Eldar Abusalimov (eldar.abusalimov@gmail.com)
	 */
	private static class TransfersHandler extends ChildHierarchyHandler {

		@Override
		protected HierarchyHandler onStartChild(String name, Attributes attrs)
				throws SAXException {
			if (name.equals(TAG_TRANSFER)) {
				try {
					int from = Integer.parseInt(attrs.getValue(ATTR_FROM));
					int to = Integer.parseInt(attrs.getValue(ATTR_TO));

					int time = 0;
					try {
						time = Time.stringToSeconds(attrs.getValue(ATTR_TIME));
					} catch (TimeFormatException e) {
						// noop
					}

					getBuilder().addTransfer(from, to, time);
				} catch (NumberFormatException e) {
					// noop
				}

			}
			return this;
		}

	}

	/**
	 * @author Eldar Abusalimov (eldar.abusalimov@gmail.com)
	 */
	private static class DescriptionHandler extends ChildHierarchyHandler {

		@Override
		protected HierarchyHandler onStartChild(String name, Attributes attrs)
				throws SAXException {
			if (name.equals(TAG_CONTENT)) {
				// TODO last-modified attribute parsing

			}
			if (!VERSION_0_6.equals(getVersion())) {
				if (name.equals(TAG_CITY)) {
					return new CityHandler();

				}
			}
			return this;
		}

		@Override
		protected void onNonWhiteString(String name, String string)
				throws SAXException {
			if (name.equals(TAG_NAME)) {
				getBuilder().addAuthorName(string);

			} else if (name.equals(TAG_EMAIL)) {
				// TODO

			} else if (name.equals(TAG_NOTE)) {
				getBuilder().setNote(string);

			}
		}

	}

	/**
	 * @author Eldar Abusalimov (eldar.abusalimov@gmail.com)
	 */
	private static class CityHandler extends ChildHierarchyHandler {

		@Override
		protected HierarchyHandler onStartChild(String name, Attributes attrs)
				throws SAXException {
			if (VERSION_0_6.equals(getVersion())) {
				if (name.equals(TAG_COUNTRY)) {
					getBuilder().setCountryCode(
							attrs.getValue(ATTR_COUNTRY_CODE));
				} else if (name.equals(TAG_LOCATION)) {
					try {
						int latitude = Integer.parseInt(attrs
								.getValue(ATTR_LATITUDE));
						int longitude = Integer.parseInt(attrs
								.getValue(ATTR_LONGITUDE));
						getBuilder().setCityLocation(latitude, longitude);
					} catch (NumberFormatException e) {
						// noop
					}

				}
			}
			return this;
		}

		@Override
		protected void onNonWhiteString(String name, String string)
				throws SAXException {
			if (name.equals(TAG_NAME)) {
				getBuilder().setCityName(string);

			} else if (name.equals(TAG_ALIAS)) {
				getBuilder().addCityAlias(string);

			} else if (name.equals(TAG_COUNTRY)) {
				getBuilder().setCountryName(string);

			}
		}

	}

	/**
	 * @author Eldar Abusalimov (eldar.abusalimov@gmail.com)
	 */
	private static class SilentHandler extends ChildHierarchyHandler {
	}

}
